package cn.iocoder.yudao.module.infra.framework.hutool;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.util.ReUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.*;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.framework.common.util.json.JsonValidator;
import cn.iocoder.yudao.framework.common.util.system.IPUtil;
import cn.iocoder.yudao.module.infra.api.logger.ApiAccessLogApi;
import cn.iocoder.yudao.module.infra.api.logger.dto.ApiAccessLogCreateReqDTO;
import cn.iocoder.yudao.module.infra.constants.InfraHeaderConstants;
import cn.iocoder.yudao.module.infra.util.TraceIdUtil;
import com.alibaba.ttl.TransmittableThreadLocal;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

@Component
@Slf4j
public class HuToolHttpInterceptor {


    private final ThreadLocal<HttpRequest> requestCache = new TransmittableThreadLocal();
    private final ThreadLocal<LocalDateTime> beginTimeCache = new TransmittableThreadLocal();
    private final HttpInterceptor<HttpRequest> requestInterceptor = new RequestInterceptor(requestCache, beginTimeCache);
    private final HttpInterceptor<HttpResponse> responseInterceptor = new ResponseInterceptor(requestCache, beginTimeCache);

    @Resource
    private ApiAccessLogApi apiAccessLogApi;

    @Value("${spring.application.name}")
    private String applicationName;

    @Resource
    private HuToolHttpAccessLogConfig huToolHttpAccessLogConfig;

    @Value("${yudao.access-log.enabled:false}")
    private boolean accessLog;

    @PostConstruct
    public void init() {
        log.info("添加HuToolHttpInterceptor, accessLog = {}, huToolHttpConfig = {}", accessLog, huToolHttpAccessLogConfig);
        if (accessLog) {
            GlobalInterceptor.INSTANCE.addRequestInterceptor(requestInterceptor);
            GlobalInterceptor.INSTANCE.addResponseInterceptor(responseInterceptor);
        }
    }

    public static class RequestInterceptor implements HttpInterceptor<HttpRequest> {
        private final ThreadLocal<HttpRequest> requestCache;
        private final ThreadLocal<LocalDateTime> beginTimeCache;

        public RequestInterceptor(ThreadLocal<HttpRequest> requestCache, ThreadLocal<LocalDateTime> beginTimeCache) {
            this.requestCache = requestCache;
            this.beginTimeCache = beginTimeCache;
        }

        @Override
        public void process(HttpRequest request) {
            try {
                final LocalDateTime beginTime = LocalDateTime.now();
                beginTimeCache.set(beginTime);
                requestCache.set(request);

                final String url = request.getUrl();
                final Map<String, List<String>> headers = getHeaderWithRemoveNullKey(request);
                final Method method = request.getMethod();
                final String body = getRequestBody(request);

                log.info("Http请求OUT-开始, url = {}, method = {}, request.body = {}, request.headers = {}",
                        url, method, body, JsonUtils.toJsonString(headers));
            } catch (Exception e) {
                log.warn("Http请求OUT-拦截器异常", e);
            }
        }

    }


    public class ResponseInterceptor implements HttpInterceptor<HttpResponse> {
        private final ThreadLocal<HttpRequest> requestCache;
        private final ThreadLocal<LocalDateTime> beginTimeCache;

        public ResponseInterceptor(ThreadLocal<HttpRequest> requestCache, ThreadLocal<LocalDateTime> beginTimeCache) {
            this.requestCache = requestCache;
            this.beginTimeCache = beginTimeCache;
        }

        @Override
        public void process(HttpResponse response) {
            try {
                final int statusCode = response.getStatus();
                final Map<String, List<String>> headers = getHeaderWithRemoveNullKey(response);
                final String body = response.body();

                final HttpRequest request = requestCache.get();
                requestCache.remove();

                final String url = request.getUrl();
                boolean isAws = url.contains("amazonaws.com");
                if (response.isOk()) {
                    log.info("Http请求OUT-成功, url = {}, statusCode = {}, response.body = {}, response.headers = {}",
                            url, statusCode, isAws ? "file content ***" : body, JsonUtils.toJsonString(headers));
                } else {
                    log.warn("Http请求OUT-失败, url = {}, statusCode = {}, response.body = {}, response.headers = {}",
                            url, statusCode, body, JsonUtils.toJsonString(headers));
                }

                final boolean inBlacklist = isInBlacklist(url);
                if (!inBlacklist) {
                    try {
                        final ApiAccessLogCreateReqDTO accessLog = createApiAccessLog(request, beginTimeCache,
                                response);
                        apiAccessLogApi.createApiAccessLog(accessLog);
                    } catch (Exception e) {
                        log.error("创建ApiAccessLog失败", e);
                    }
                }
            } catch (Exception e) {
                log.warn("Http请求OUT-拦截器异常", e);
            }
        }
    }

    private boolean isInBlacklist(String url) {
        final List<String> blacklistUrls = huToolHttpAccessLogConfig.getBlacklistUrls();
        if (CollUtil.isNotEmpty(blacklistUrls)) {
            for (String blacklistUrl : blacklistUrls) {
                try {
                    final boolean match = ReUtil.isMatch(blacklistUrl, url);
                    if (match) {
                        return true;
                    }
                } catch (Exception e) {
                    log.error(String.format("match url error, url = %s, blacklistUrl = %s", url, blacklistUrl), e);
                }
            }
        }
        return false;
    }

    private ApiAccessLogCreateReqDTO createApiAccessLog(HttpRequest request,
                                                        ThreadLocal<LocalDateTime> beginTimeCache,
                                                        HttpResponse response) {
        final ApiAccessLogCreateReqDTO accessLog = new ApiAccessLogCreateReqDTO();
        accessLog.setDirection("OUT");

        // requestId
        String traceId = request.header(InfraHeaderConstants.HEADER_TRACE_ID);
        if (StrUtil.isEmpty(traceId)) {
            traceId = TraceIdUtil.getTraceId();
        }
        accessLog.setTraceId(traceId);

        // businessId
        final String businessId = request.header(InfraHeaderConstants.HEADER_BUSINESS_ID);
        accessLog.setBusinessId(businessId);

        accessLog.setUserId(0L);
        accessLog.setUserType(0);
        accessLog.setApplicationName(applicationName);

        accessLog.setRequestMethod(request.getMethod().name());
        accessLog.setRequestUrl(request.getUrl());
        final Map<String, List<String>> requestHeaders = getHeaderWithRemoveNullKey(request);
        accessLog.setRequestHeader(JsonUtils.toJsonString(requestHeaders));
        final String requestParamsJsonString = toRequestParamsJsonString(request);
        accessLog.setRequestParams(requestParamsJsonString);
        final String localIPv4Address = IPUtil.getLocalIPv4Address();
        accessLog.setUserIp(localIPv4Address);
        final String userAgent = getUserAgent(request);
        accessLog.setUserAgent(userAgent);

        final LocalDateTime beginTime = beginTimeCache.get();
        beginTimeCache.remove();
        accessLog.setBeginTime(beginTime);
        accessLog.setEndTime(LocalDateTime.now());
        accessLog.setDuration(
                (int) LocalDateTimeUtil.between(
                        accessLog.getBeginTime(), accessLog.getEndTime(), ChronoUnit.MILLIS
                )
        );

        final int statusCode = response.getStatus();
        accessLog.setResultCode(0);
        accessLog.setResponseStatusCode(statusCode);
        final Map<String, List<String>> responseHeaders = getHeaderWithRemoveNullKey(response);
        accessLog.setResponseHeader(JsonUtils.toJsonString(responseHeaders));
        final String responseBodyJsonString = toResponseBodyJsonString(response.body());
        accessLog.setResponseBody(responseBodyJsonString);
        return accessLog;
    }

    private static String toResponseBodyJsonString(String responseBody) {
        if (JsonValidator.isValidJson(responseBody)) {
            return responseBody;
        }

        final Map<String, String> params = new HashMap<>();
        params.put("responseBody", responseBody);
        return JsonUtils.toJsonString(params);
    }

    private static String toRequestParamsJsonString(HttpRequest request) {
        final String requestParams = getRequestBody(request);
        if (JsonValidator.isValidJson(requestParams)) {
            return requestParams;
        }

        final Map<String, String> params = new HashMap<>();
        params.put("requestParams", requestParams);
        return JsonUtils.toJsonString(params);
    }

    private static String getRequestBody(HttpRequest request) {
        String body = null;
        final String contentType = request.header(Header.CONTENT_TYPE);
        if (StrUtil.containsAnyIgnoreCase(contentType, "json")) {
            final byte[] bytes = request.bodyBytes();
            body = StrUtil.utf8Str(bytes);
        }
        return body;
    }

    private static String getUserAgent(HttpRequest request) {
        String ua = request.header(Header.USER_AGENT);
        return ua != null ? ua : "";
    }

    private static Map<String, List<String>> getHeaderWithRemoveNullKey(HttpRequest request) {
        final Map<String, List<String>> headers = new HashMap<>(request.headers());
        removeNullKey(headers);
        return headers;
    }

    private static Map<String, List<String>> getHeaderWithRemoveNullKey(HttpResponse response) {
        final Map<String, List<String>> headers = new HashMap<>(response.headers());
        removeNullKey(headers);
        return headers;
    }

    private static void removeNullKey(Map<String, List<String>> headers) {
        if (CollUtil.isNotEmpty(headers)) {
            Iterator<Map.Entry<String, List<String>>> iterator = headers.entrySet().iterator();
            while (iterator.hasNext()) {
                final Map.Entry<String, List<String>> entry = iterator.next();
                if (StrUtil.isEmpty(entry.getKey())) {
                    iterator.remove();
                }
            }
        }
    }

}
